Pointer Eventでお絵かきアプリを作ってみる
あとは単純に作ってみたいという願望も動機
あとやりたいこと
https://gyazo.com/337ce6c8d4e85049f54293d345793a35
動かなかった部分を少し調整しただけ
ならこれをさらに書き換えてみるか
Scrapbox上で起動するようにしよう
2022-05-21
15:22:20 smartphoneで試したらうまく動かなかった
画面外タッチでアプリが閉じない
マルチタッチに対応していない
2022-04-15
何故か線がガクガクになる
何が原因だろうか?
マウスでも重くなる
どうやらたくさん描くと重くなるようだ
19:15:37 別の実装にしてみた
19:21:03 違うっぽい。あまり変わらなかった
余計な描画が挟まってしまっている?
19:22:25 .messageを消してみる
19:29:01 改善したかも?
描画がscroll eventとして<body>に拾われてしまう なるほど。移動前の場所がクリックできなくなってしまうのか
要素判定されてしまう
18:39:01 色々直した
18:12:19 書けるようになった
マウスでのみ確かめた
バグ
結果、ずれた位置に描画されてしまう
単に<canvas>の基準座標とPointerEventの基準座標があっていないだけだった
そりゃずれるよ
補正しなきゃ
マウスボタンを離すと画面が消えてしまう
これはマウスクリックを:hostで検出してしまったため
:host .containerで発生したクリックを伝播させないようにすればいい
code:ts
app.addEventListener("click", () => app.style.display = "none");
// ↑のlistenerで検出しないようにする
container.addEventListener("click", (e) => e.stopPropagation());
code:sh
code:script.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
import { setup } from "./mod.ts";
import type { Scrapbox } from "../scrapbox-jp%2Ftypes/mod.ts";
declare const scrapbox: Scrapbox;
const { open, close } = setup();
let opened = false;
scrapbox.PageMenu.addItem({
title: () => !opened ? "Start painting" : "End painting",
onClick: () => {
if (opened) {
close();
opened = false;
return;
}
open();
opened = true;
},
});
code:mod.ts
import { colorForTouch } from "./color.ts";
export const setup = () => {
const app = document.createElement("div");
const shadowRoot = app.attachShadow({ mode: "open" });
const container = document.createElement("div");
container.classList.add("container");
/*const message = document.createElement("div");
message.classList.add("message");
const log = (msg: string) => {
message.innerText = ${msg}\n${message.innerText};
};*/
const canvas = document.createElement("canvas");
canvas.width = 600;
canvas.height = 300;
// 複数のpointersが検出される可能性があるので、複数のevent loopsが並行して動作できるようにする
canvas.addEventListener("pointerdown", (e) => eventLoop(e, canvas, console.log), false);
//container.append(canvas, "Log: ", message);
container.append(canvas);
shadowRoot.appendChild(makeStyle());
shadowRoot.appendChild(container);
app.style.display = "none";
document.body.append(app);
return {
open: () => app.style.display = "block",
close: () => app.style.display = "none",
};
};
const eventLoop = async (
startEvent: PointerEvent,
canvas: HTMLCanvasElement,
log: (text: string) => void,
): Promise<void> => {
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2D context is not supported.");
log(pointerdown: id = ${startEvent.pointerId});
// <canvs>の外に出ても、ボタンを離したりdeviceを取り外したりしない限り描画を継続させる
canvas.setPointerCapture(startEvent.pointerId);
// draw a circle at the start
const start = relative(startEvent, canvas);
ctx.beginPath();
ctx.arc(start.x, start.y, 4, 0, 2 * Math.PI, false); // a circle at the start
const color = colorForTouch(startEvent, log);
ctx.fillStyle = color;
ctx.fill();
// draw a path
let prev = start;
const handleMove = (event: PointerEvent) => {
ctx.beginPath();
log(ctx.moveTo(${prev.x}, ${prev.y}););
ctx.moveTo(prev.x, prev.y);
const next = relative(event, canvas);
log(ctx.lineTo(${next.x}, ${next.y}););
ctx.lineTo(next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color;
ctx.stroke();
prev = next;
log(".");
};
// いずれかのeventが発火したら描画を終了する
let handleEnd: (event: PointerEvent) => void = () => {};
const pending = new Promise<PointerEvent>((resolve) => handleEnd = resolve);
canvas.addEventListener("pointerup", handleEnd, false);
canvas.addEventListener("pointercancel", handleEnd, false);
canvas.addEventListener("pointerout", handleEnd, false);
canvas.addEventListener("pointermove", handleMove, false);
const endEvent = await pending;
canvas.removeEventListener("pointermove", handleMove, false);
canvas.removeEventListener("pointerup", handleEnd, false);
canvas.removeEventListener("pointercancel", handleEnd, false);
canvas.removeEventListener("pointerout", handleEnd, false);
if (endEvent.type === "pointerup") {
// and a square at the end
log(endEvent.type);
ctx.lineWidth = 4;
ctx.fillStyle = color;
ctx.beginPath();
const end = relative(endEvent, canvas);
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.fillRect(end.x - 4, end.y - 4, 8, 8);
} else {
log(${endEvent.type}: id = ${endEvent.pointerId});
}
};
const relative = (event: PointerEvent, base: HTMLElement): { x: number; y: number; } => {
const rect = base.getBoundingClientRect();
return {
x: Math.round(event.clientX - rect.left),
y: Math.round(event.clientY - rect.top),
};
};
const makeStyle = (): HTMLStyleElement => {
const style = document.createElement("style");
style.textContent =
`:host {
position: fixed;
top: 0;
left: 0;
z-index: 1050;
outline: 0;
}
.container {
position: relative;
display: flex;
flex-direction: column;
width: 600px;
margin: 30px auto;
max-height: calc(100vh - 60px);
transform: translateX(calc(50vw - 50%));
background-color: var(--dropdown-menu-bg, #fff); border: 1px solid rgba(0,0,0,.2);
border-radius: 6px;
background-clip: padding-box;
outline: 0;
}
canvas {
border: 1px solid black;
touch-action: none;
}
.message {
border-radius: 6px;
overflow-x: hidden;
overflow-y: scroll;
}`;
return style;
};
code:color.ts
export const colorForTouch = (
touch: PointerEvent,
log: (text: string) => void,
): #${string} => {
const r = (touch.pointerId % 16).toString(16);
const g = (Math.floor(touch.pointerId / 3) % 16).toString(16);
const b = (Math.floor(touch.pointerId / 7) % 16).toString(16);
const color = #${r}${g}${b} as const;
log(color for touch with identifier ${touch.pointerId} = ${color});
return color;
};